#include "PreCompile.hpp"
#include "InStream.hpp"
#include "StringBuilder.hpp"

namespace BuildNG
{

  ///////////
  // InStream
  InStream::InStream(int buffer_size)
  {
    assert(buffer_size >= 1 && buffer_size <= max_buffer_size);

    allocated_buffer_size = buffer_size;

    buffer = new char[allocated_buffer_size];
    read_ptr = buffer;
    cur_buffer_end = buffer;

    ended_with_newline = false;
  }

  InStream::~InStream()
  {
    delete [] buffer;
  }

  void InStream::fill_buffer()
  {
    size_t new_buffer_size = read_impl(buffer,allocated_buffer_size);
    read_ptr = buffer;
    cur_buffer_end = buffer + new_buffer_size;
  }

  void InStream::read_buffer(void *dest,int byte_size)
  {
    assert(byte_size >= 0);

    char *write_ptr = (char*)dest;
    size_t size_left = (size_t)byte_size;

    // if available, copy data from the buffer
    size_t from_buffer_size = std::min<size_t>(cur_buffer_end - read_ptr,size_left);
    memcpy(write_ptr,read_ptr,from_buffer_size);
    write_ptr += from_buffer_size;
    size_left -= from_buffer_size;
    read_ptr += from_buffer_size;

    // if size_left == 0, we have read enough, so we can return.
    if(size_left == 0)
      return;

    // if we get here, it means that we read all the data available
    // in the buffer, but it's not enough yet.

    if(size_left < allocated_buffer_size)
    {
      // read a new buffer
      size_t new_buffer_size = read_impl(buffer,allocated_buffer_size);
      cur_buffer_end = buffer + new_buffer_size;
      if(size_left > new_buffer_size)
        throw InputPastEndOfFileException(*this);

      // get the remaining part, from the buffer we just read.
      memcpy(write_ptr,buffer,size_left);
      read_ptr = buffer + size_left;
    }
    else
    {
      // the mem block is so big, that it's better to just ignore the
      // buffer, and read the data directly into the destination memory.

      size_t size_read = read_impl(write_ptr,size_left);
      if(size_read != size_left)
      {
        throw InputPastEndOfFileException(*this);
      }
    }
  }

  guint32 InStream::read_uint32()
  {
    guint32 ret;
    read_buffer(&ret,sizeof(guint32));
    return ret;
  }

  gint32 InStream::read_int32()
  {
    gint32 ret;
    read_buffer(&ret,sizeof(gint32));
    return ret;
  }

  guint16 InStream::read_uint16()
  {
    guint16 ret;
    read_buffer(&ret,sizeof(guint16));
    return ret;
  }

  gint16 InStream::read_int16()
  {
    gint16 ret;
    read_buffer(&ret,sizeof(gint16));
    return ret;
  }

  guint8 InStream::read_uint8()
  {
    guint8 ret;
    read_buffer(&ret,sizeof(guint8));
    return ret;
  }

  gint8 InStream::read_int8()
  {
    gint8 ret;
    read_buffer(&ret,sizeof(guint8));
    return ret;
  }

  bool InStream::read_bool()
  {
    return read_uint8() != 0;
  }

  bool InStream::read_line(Glib::ustring &line)
  {
    StringBuilder<256> res_builder;
    line.clear();

    if(read_ptr == cur_buffer_end)
    {
      fill_buffer();

      // if still nothing, we are at the end of the file, so return false.
      if(read_ptr == cur_buffer_end)
      {
        if(ended_with_newline)
        {
          ended_with_newline = false;
          return true;
        }
        else
          return false;
      }
    }

    // the other newline character of the new line char pair
    // on windows (\r\n), this would be initialized when a \r is
    // encountered, and set to \n, because the \n is the next character we
    // expect.
    gchar other_newlinechar_of_pair = 0;
    const gchar *begin = read_ptr;
    while(1)
    {
      assert(read_ptr != cur_buffer_end);
      if(*read_ptr == '\n')
      {
        res_builder.append(begin,read_ptr);

        other_newlinechar_of_pair = '\r';
        read_ptr++;
        break;
      }
      else if(*read_ptr == '\r')
      {
        res_builder.append(begin,read_ptr);

        other_newlinechar_of_pair = '\n';
        read_ptr++;
        break;
      }

      read_ptr++;

      if(read_ptr == cur_buffer_end)
      {
        res_builder.append(begin,read_ptr);

        fill_buffer();
        if(read_ptr == cur_buffer_end)
          break;
        begin = read_ptr;
      }
    }

    if(other_newlinechar_of_pair)
    {
      bool has_data;
      if(read_ptr == cur_buffer_end)
      {
        fill_buffer();
        has_data = read_ptr != cur_buffer_end;
        if(!has_data)
          ended_with_newline = true;
      }
      else
        has_data = true;

      if(has_data)
      {
        if(*read_ptr == other_newlinechar_of_pair)
          read_ptr++;

        if(read_ptr == cur_buffer_end)
        {
          fill_buffer();
          if(read_ptr == cur_buffer_end)
            ended_with_newline = true;
        }
      }
    }

    if(!res_builder.validate_utf8())
      throw InStreamInvalidUTF8Exception(*this);

    line = res_builder.to_ustring();


    return true;
  }

  ////////
  // MemoryInStream
  MemoryInStream::MemoryInStream(const void *data,size_t data_size,int flags,int buffer_size)
    : InStream(buffer_size)
  {
    this->data = (const char*)data;
    this->flags = flags;

    read_ptr = (const char*)data;
    size_left = data_size;
  }

  MemoryInStream::MemoryInStream(const gchar *null_term_string)
  {
    this->data = null_term_string;
    this->flags = 0;
    read_ptr = data;
    size_left = strlen(null_term_string);
  }

  MemoryInStream::~MemoryInStream()
  {
    if(flags & Flag_DeleteData)
      delete [] data;
  }

  size_t MemoryInStream::read_impl(char *buffer,size_t max_size)
  {
    size_t size_to_read = std::min(max_size,size_left);
    memcpy(buffer,read_ptr,size_to_read);
    read_ptr += size_to_read;
    size_left -= size_to_read;

    return size_to_read;
  }

  ///////////////
  // FileInStream
  FileInStream::FileInStream(const Glib::ustring &file_name)
  {
    file = fopen(file_name.c_str(),"rb");
    if(!file)
      throw FileNotFoundException(file_name);

    this->file_name = file_name;
  }

  FileInStream::~FileInStream()
  {
    fclose(file);
  }

  size_t FileInStream::read_impl(char *buffer,size_t max_size)
  {
    size_t num_bytes_read = fread(buffer,1,max_size,file);
    return num_bytes_read;
  }

  Glib::ustring FileInStream::get_stream_desc() const
  {
    return file_name;
  }

  ////////////
  // GIOChannelInStream
  size_t GIOChannelInStream::read_impl(char *buffer,size_t max_size)
  {
  again:

    gsize num_read;
    Glib::IOStatus status = io_channel->read(buffer,(gsize)max_size,num_read);
    switch(status)
    {
    case Glib::IO_STATUS_ERROR:
      throw InStreamException("Couldn't read from the IOChannel.",*this);

    case Glib::IO_STATUS_NORMAL:
      return num_read;

    case Glib::IO_STATUS_EOF:
      return 0;

    case Glib::IO_STATUS_AGAIN:
      goto again;

    default:
      assert(!"unknown IOStatus");
      return 0;
    }
  }

  Glib::ustring GIOChannelInStream::get_stream_desc() const
  {
    return "a Glib::IOChannel";
  }

  /////////
  // StdInStream  
  StdInStream::StdInStream(std::istream *stream,bool take_ownership)
  {
    this->stream = stream;
    owned = take_ownership;
  }

  StdInStream::~StdInStream()
  {
    if(owned)
    {
      delete stream;
    }
  }

  size_t StdInStream::read_impl(char *buffer,size_t max_size)
  {
    if(!*stream)
      return 0;

    stream->read(buffer,(std::streamsize)max_size);
    if(stream->bad())
    {
      throw InStreamException("Couldn't read from the stream.",*this);
    }
    
    return (size_t)stream->gcount();
  }

  ///////////////////
  // Exceptions
  InStreamException::InStreamException(const Glib::ustring &msg,const InStream &stream)
    : Exception(create_message(msg,stream))
  {
  }

  Glib::ustring InStreamException::create_message(const Glib::ustring &msg,const InStream &stream)
  {
    Glib::ustring ret;

    Glib::ustring stream_desc = stream.get_stream_desc();
    if(stream_desc != "")
    {
      ret = "Error while reading from " + stream_desc + ": " + msg;
    }
    else
    {
      ret = msg;
    }

    return ret;
  }

  InputPastEndOfFileException::InputPastEndOfFileException(const InStream &stream)
    : InStreamException("Input past end of file.",stream)
  {
  }

  FileNotFoundException::FileNotFoundException(const Glib::ustring &file_name)
    : Exception(create_message(file_name))
  {
  }

  Glib::ustring FileNotFoundException::create_message(const Glib::ustring &file_name)
  {
    return "Couldn't open the file \"" + file_name + "\".";
  }

  InStreamInvalidUTF8Exception::InStreamInvalidUTF8Exception(const InStream &stream)
    : InStreamException("Invalid UTF8 while trying to read a line of text.",stream)
  {
  }
}
